redis 对象信息
struct RedisObject {
int4 type; // 4bits 什么数据类型,比如字符串,hash,set之类的
int4 encoding; // 4bits 比如字符串用什么方式存储,比如emb,raw.
int24 lru; // 24bits 前文说过关于内存不够是采用的lru算法,这个是保存的是上一次使用的时间戳。
int32 refcount; // 4bytes 这个是对象的引用计数,当这个计数为0的时候,这个对象就要被回收。
void *ptr; // 8bytes,64-bit system 这个保存的实际上对象的存储位置。
} robj;
String
其实现是一个带长度信息的字节数组
struct SDS<T> {
T capacity; // 数组容量
T len; // 数组长度
byte flags; // 特殊标识位,不理睬它
byte[] content; // 数组内容
}
很类似于java里面ArrayList的实现,一个动态数组。
redis字符串有2种存储方式,一个是长度小于44时用emb方式存储,当长度大于44时采用raw方式存储。
为何?
前面说了,每个对象头的数据结构大约是16字节,字符串自身数据结构是>=3字节,字符串以\0结尾,一个字节,因此长度为20字节,内存分配器分配内存都是2的幂次,为了存储redis字符串,起码要32,稍微长点就是64,而redis认为大于64字节就是大字符,而64-20=44,因此当数据量小的时候采用emb,方式,就是对象头和sds放在一起,分配一次内存即可,当大约44时就采用raw模式,对象头和sds分开,分配2次内存即可。
字典内部实现
字典这个数据结构在redis里面用处很频繁,hash结构,所有的redis里面的key和value,带过期时间的key集合,zset里面value和score的集合
struct RedisDb {
dict* dict; // all keys key=>value
dict* expires; // all expired keys key=>long(timestamp)
}
struct zset {
dict *dict; // all values value=>score
zskiplist *zsl;
}
dict书籍结构包含2个hashtable,因为redis的扩容时渐进的。因此同时存在2个hashtable,这个迁移主要来自客户端的hset/hdel指令等,当然还有定时任务来配合。
压缩列表
前面说过了,当hashmap,zset数据量小的时候回采用ziplist,这个压缩列表来存储,将2维变成一维。
struct ziplist<T> {
int32 zlbytes; // 整个压缩列表占用字节数
int32 zltail_offset; // 最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个节点
int16 zllength; // 元素个数
T[] entries; // 元素内容列表,挨个挨个紧凑存储
int8 zlend; // 标志压缩列表的结束,值恒为 0xFF
}
struct entry {
int<var> prevlen; // 前一个 entry 的字节长度
int<var> encoding; // 元素类型编码
optional byte[] content; // 元素内容
}
压缩列表为了支持双向遍历,所以才会有 ztail_offset 这个字段,用来快速定位到最后一个元素,然后倒着遍历。
当 set 集合容纳的元素都是整数并且元素个数较小时,Redis 会使用 intset 来存储结合元素。intset 是紧凑的数组结构,同时支持 16 位、32 位和 64 位整数
struct intset<T> {
int32 encoding; // 决定整数位宽是 16 位、32 位还是 64 位
int32 length; // 元素个数
int<T> contents; // 整数数组,可以是 16 位、32 位和 64 位
}
快速列表
quicklist 是 ziplist 和 linkedlist 的混合体,它将 linkedlist 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来。
struct ziplist {
...
}
struct ziplist_compressed {
int32 size;
byte[] compressed_data;
}
struct quicklistNode {
quicklistNode* prev;
quicklistNode* next;
ziplist* zl; // 指向压缩列表
int32 size; // ziplist 的字节总数
int16 count; // ziplist 中的元素数量
int2 encoding; // 存储形式 2bit,原生字节数组还是 LZF 压缩存储
}
struct quicklist {
quicklistNode* head;
quicklistNode* tail;
long count; // 元素总数
int nodes; // ziplist 节点的个数
int compressDepth; // LZF 算法压缩深度
...
}
quicklist 内部默认单个 ziplist 长度为 8k 字节,超出了这个字节数,就会新起一个 ziplist。ziplist 的长度由配置参数list-max-ziplist-size决定。
zset——跳跃表
个人觉得跳跃表的查找方式类似于B+树。
struct zslnode {
string value;
double score;
zslnode*[] forwards; // 多层连接指针
zslnode* backward; // 回溯指针
}
struct zsl {
zslnode* header; // 跳跃列表头指针
int maxLevel; // 跳跃列表当前的最高层
map<string, zslnode*> ht; // hash 结构的所有键值对
}
每个节点 之间使用指针串起来形成了双向链表结构,它们是 有序 排列的,从小到大。不同的 节点 层高可能不一样,层数越高的 节点 越少。同一层的 节点 会使用指针串起来。每一个层元素的遍历都是从 节点header 出发。
紧凑列表
struct listpack<T> {
int32 total_bytes; // 占用的总字节数
int16 size; // 元素个数
T[] entries; // 紧凑排列的元素列表
int8 end; // 同 zlend 一样,恒为 0xFF
}
struct lpentry {
int<var> encoding;
optional byte[] content;
int<var> length;//本元素大小
}
首先这个 listpack 跟 ziplist 的结构几乎一摸一样,只是少了一个zltail_offset字段。ziplist 通过这个字段来定位出最后一个元素的位置,用于逆序遍历。不过 listpack 可以通过其它方式来定位出最后一个元素的位置(这个位置可以通过total_bytes字段和最后一个元素的长度字段计算出来。首先通过total_bytes找到最后的标记位end,占一个字符,去除这个字符,前面就是最后一个元素,而元素length放在了后面,那end的前面就是entry的length,length又是通过固定编码方式存储,要读取出来并不难),所以zltail_offset字段就省掉了。
基数树
你可以将一本英语字典看成一棵 radix tree,它所有的单词都是按照字典序进行排列,每个词汇都会附带一个解释,这个解释就是 key 对应的 value。有了这棵树,你就可以快速地检索单词,还可以查询以某个前缀开头的单词有哪些。你也可以将公安局的人员档案信息看成一棵 radix tree,它的 key 是每个人的身份证号,value 是这个人的履历。因为身份证号的编码的前缀是按照地区进行一级一级划分的,这点和单词非常类似。有了这棵树,你就可以快速地定位出人员档案,还可以快速查询出某个小片区都有哪些人。
Rax 被用在 Redis Stream 结构里面用于存储消息队列,在 Stream 里面消息 ID 的前缀是时间戳 + 序号,这样的消息可以理解为时间序列消息。使用 Rax 结构进行存储就可以快速地根据消息 ID 定位到具体的消息,然后继续遍历指定消息之后的所有消息。
Rax 被用在 Redis Cluster 中用来记录槽位和key的对应关系,这个对应关系的变量名成叫着slots_to_keys。这个raxNode的key是由槽位编号hashslot和key组合而成的。我们知道cluster的槽位数量是16384,它需要2个字节来表示,所以rax节点里存的key就是2个字节的hashslot和对象key拼接起来的字符串。